-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support routing to Kubernetes clusters by request path #50567
Conversation
This implements path-based routing for Kubernetes clusters as described by [RFD0185]. A new prefixed path handler is added that accepts base64-encoded Teleport and Kubernetes cluster names. The request is routed to the destination Teleport cluster using these parameters instead of those embedded in the session TLS identity, and then the preexisting handlers check authorization and complete the request as usual. This removes the need for certificates to be issued per Kubernetes cluster: so long as the incoming identity is granted access to the cluster via its roles, access can succeed, and no `KubernetesCluster` attribute or cert usage restrictions are needed. [RFD0185]: #47436
Outstanding TODOs:
|
return "", "", trace.Wrap(err) | ||
} | ||
|
||
// TODO: do we care to otherwise validate these results before casting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I think it would be better anw
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not totally sure what other validation steps we might want here - somehow I'd never looked into it before but we seem to impose very few restrictions on resource names. I've added a check to make sure the parameters decode to valid UTF8, at least. Downstream looks to be a map lookup from cached cluster details, so I don't expect significant risk.
Also: fixes various other review comments, adds comment explaining auth strategy.
assert: func(t *testing.T, restConfig *rest.Config) { | ||
client := pathRoutedKubeClient(t, restConfig, clusterName, "a") | ||
_, err = client.CoreV1().Pods(metav1.NamespaceDefault).List(context.Background(), metav1.ListOptions{}) | ||
require.ErrorContains(t, err, "cannot list resource") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've noticed a mild behavior change here, I think since this just wasn't possible before. Attempting to e.g. list resources on a cluster denied by roles now yields (kubectl get pods -v8
):
I0114 20:55:58.643198 22226 request.go:1212] Response Body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"kubernetes cluster \"default\" not found","reason":"Forbidden","code":403}
I0114 20:55:58.643592 22226 helpers.go:246] server response object: [{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "kubernetes cluster \"default\" not found",
"reason": "Forbidden",
"code": 403
}]
Error from server (Forbidden): kubernetes cluster "default" not found
...however, attempting to list a nonexistent cluster returns:
I0114 20:51:57.008342 10127 request.go:1212] Response Body: I0114 20:51:57.008342 10127 request.go:1212] Response Body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Kubernetes cluster \"asdf\" not found","reason":"NotFound","code":404}
E0114 20:51:57.008352 10127 memcache.go:265] couldn't get current server API group list: the server could not find the requested resource
I0114 20:51:57.008357 10127 cached_discovery.go:120] skipped caching discovery info due to the server could not find the requested resource
I0114 20:51:57.008477 10127 helpers.go:246] server response object: [{
"metadata": {},
"status": "Failure",
"message": "the server could not find the requested resource",
"reason": "NotFound",
"details": {
"causes": [
{
"reason": "UnexpectedServerResponse",
"message": "unknown"
}
]
},
"code": 404
}]
Error from server (NotFound): the server could not find the requested resource
(and a Kubernetes cluster "$name" not found"
is logged on the server)
Previously, you could only ever get certs for a cluster if the name was valid and the user is authorized to see it, and returns a consistent error otherwise before the cert is ever issued. Would we consider this a resource name oracle?
It seems like the two responses probably ought to be identical but changing them might be a mild breaking change to existing behavior (403 vs 404).
Also, don't allow path parameters to override the identity, if the contains nonempty routing params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me - although I can't claim any expertise in regards to Kubernetes Access. Let's make sure @tigrato reviews this before we merge anything.
lib/kube/proxy/forwarder.go
Outdated
return nil, trace.Wrap(err) | ||
} | ||
state := authCtx.GetAccessState(authPref) | ||
if state.MFARequired != services.MFARequiredNever && (teleportClusterName == "" || kubeCluster == "") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is checked after singleCertHandler
(and not before) rewrites the request so hopefully it will never be empty so this check doesn't seem to change anything nor preventing users from using MFA to any cluster IIUC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was attempting to keep some invariants in place, but you're right, it didn't help. I've replaced it with a check for a non-empty MFAVerified
cert field in ensureRouteNotOverwritten()
.
The thinking when session MFA is required is:
- If an MFA assertion is missing, routing info can be freely overridden and we rely on
CheckAccess()
to deny access as usual - If an MFA assertion is present and path parameters would override identity fields, deny access
- If an MFA assertion is present and path parameters match, allow access
Without this, if you were somehow able to convince auth to issue an MFA cert with empty routing parameters, you could access any cluster via path routing.
(This is assuming I've understood MFAVerified properly, at least, but I'm reasonably certain of that. If not we'll need to do some potentially messy GetAccessState()
checks.)
path = "/" + path | ||
} | ||
|
||
req.URL.Path = path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You also need to change RawPath
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now explicitly set req.URL.RawPath = ""
. The empty string should be enough to satisfy (or rather, not satisfy) httprouter's check: https://github.com/gravitational/httprouter/blob/teleport/router.go#L393
} | ||
|
||
ctx := authz.ContextWithUser(req.Context(), userType) | ||
req = req.WithContext(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
req = req.WithContext(ctx) | |
req = req.Clone(ctx) |
deep clone the r.URL because it's a pointer and it's not copied during req.WithContext otherwise we are introducing a data race.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good catch, it's now deep copied as you suggested.
Deep clone the request and explicitly clear RawPath.
@timothyb89 See the table below for backport results.
|
* Support routing to Kubernetes clusters by request path This implements path-based routing for Kubernetes clusters as described by [RFD0185]. A new prefixed path handler is added that accepts base64-encoded Teleport and Kubernetes cluster names. The request is routed to the destination Teleport cluster using these parameters instead of those embedded in the session TLS identity, and then the preexisting handlers check authorization and complete the request as usual. This removes the need for certificates to be issued per Kubernetes cluster: so long as the incoming identity is granted access to the cluster via its roles, access can succeed, and no `KubernetesCluster` attribute or cert usage restrictions are needed. [RFD0185]: #47436 * Remove routeSourcer in favor of identity values Also: fixes various other review comments, adds comment explaining auth strategy. * Revert more dead code * Revert more unnecessary changes; validate utf8 parameters * Build fixes after merge with slog transition * Add basic tests for Kubernetes path routing * Fix imports * Check identity parameters when per session MFA is enabled Also, don't allow path parameters to override the identity, if the contains nonempty routing params. * Fix failing TestSingleCertRouting due to improper reuse of rest config * Session MFA includes role based enforcement; added more test cases * Don't allow route params to be overwritten if MFAVerified is set * Code review feedback Deep clone the request and explicitly clear RawPath.
This implements path-based routing for Kubernetes clusters as described by RFD0185. A new prefixed path handler is added that accepts base64-encoded Teleport and Kubernetes cluster names. The request is routed to the destination Teleport cluster using these parameters instead of those embedded in the session TLS identity, and then the preexisting handlers check authorization and complete the request as usual.
This removes the need for certificates to be issued per Kubernetes cluster: so long as the incoming identity is granted access to the cluster via its roles, access can succeed, and no
KubernetesCluster
attribute or cert usage restrictions are needed.Fixes #40405
No changelog for this PR, will be added in #50898
Testing steps
Ensure a Teleport proxy is running this branch with one or more k8s clusters attached. The Kubernetes node does not need to be running this branch, only the proxy.
Build
tbot
against Machine ID: Support path-based Kubernetes routing #50898Create a new bot with appropriate permissions to access your kube node(s), e.g.
tctl bots add foo --roles=kube-access
Start
tbot
with the newkubernetes/v2
service:The
--kubernetes-cluster-name
flag can be repeated. Alternatively (or additionally), try a label matcher:Examine
./tbot-user/kubeconfig.yaml
. You should see onecluster:
and onecontext:
entry per matched cluster. Theserver:
field should include a new URL suffix,/v1/teleport/<teleportCluster>/<kubeCluster>
Try
KUBECONFIG=./tbot-user/kubeconfig.yaml kubectl get pods
. Also consider testingkubectl exec
, port forward, etc.